using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;

namespace SharpObjects.XpathToObjects
{
    public class XpathProcessor : IXpathProcessor
    {
        private IXPathParser parser;


        public XpathProcessor()
        {
            this.parser = new XPathParser();
        }

        public XpathProcessor(IXPathParser xPathParser)
        {
            if(xPathParser == null)
            {
                this.parser = new XPathParser();
            }
            else
            {
                this.parser = xPathParser;
            }
        }


        #region IXpathProcessor Members


        public T SelectObject<T>(object root, string xpath)
        {
            List<PathElement> path = parser.Parse(xpath);
            return SelectObject<T>(root, path);
        }

        public T SelectObject<T>(object root, List<PathElement> xpath)
        {
            if(root == null)
            {
                throw new ArgumentException("root argument must not be null");
            }

            if(xpath == null || xpath.Count == 0)
            {
                throw new ArgumentException("XPath must not be null or empty");
            }

            List<object> objectBranch = new List<object>();
            objectBranch.Add(root);

            return (T) SelectObject(objectBranch, xpath, 0);
        }

        public T SelectObject<T>(List<object> objectBranch, string xpath)
        {
            List<PathElement> path = parser.Parse(xpath);
            return SelectObject<T>(objectBranch, path);
        }

        public T SelectObject<T>(List<object> objectBranch, List<PathElement> xpath)
        {
            if(objectBranch == null || objectBranch.Count == 0)
            {
                throw new ArgumentException("objectBranch argument must not be null or empty");
            }

            if (xpath == null || xpath.Count == 0)
            {
                throw new ArgumentException("XPath must not be null or empty");
            }

            return (T)SelectObject(objectBranch, xpath, 0);
        }



        public List<T> SelectObjects<T>(object root, string xpath)
        {
            List<PathElement> path = parser.Parse(xpath);
            return SelectObjects<T>(root, path);
        }

        public List<T> SelectObjects<T>(object root, List<PathElement> xpath)
        {
            if (root == null)
            {
                throw new ArgumentException("root argument must not be null");
            }

            if (xpath == null || xpath.Count == 0)
            {
                throw new ArgumentException("XPath must not be null or empty");
            }

            //TODO implement multiple object selection
            throw new NotImplementedException("Only single object selection is implemented");
        }

        public List<T> SelectObjects<T>(List<object> objectBranch, string xpath)
        {
            List<PathElement> path = parser.Parse(xpath);
            return SelectObjects<T>(objectBranch, path);
        }

        public List<T> SelectObjects<T>(List<object> objectBranch, List<PathElement> xpath)
        {
            if (objectBranch == null || objectBranch.Count == 0)
            {
                throw new ArgumentException("objectBranch argument must not be null or empty");
            }

            if (xpath == null || xpath.Count == 0)
            {
                throw new ArgumentException("XPath must not be null or empty");
            }

            //TODO implement multiple object selection
            throw new NotImplementedException("Only single object selection is implemented");
        }


        #endregion IXpathProcessor Members





        private object SelectObject(List<object> objectTrace, List<PathElement> xpath, int xPathElementIndex)
        {
            if (xpath[xPathElementIndex].ElementType == PathElement.ePathElementType.Object)
            {
                #region Object Name

                object value = GetPropertyValue(objectTrace.Last(), (PathElementObject)xpath[xPathElementIndex]);

                if(value == null)
                {
                    return null;
                }

                if ((xPathElementIndex + 1) < xpath.Count)
                {
                    objectTrace.Add(value);
                    return SelectObject(objectTrace, xpath, xPathElementIndex + 1);
                }
                else
                {
                    return value;
                }

                #endregion Object Name
            }
            else if(xpath[xPathElementIndex].ElementType == PathElement.ePathElementType.AnyName)
            {
                #region Any name (*)

                object obj = objectTrace.Last();

                PropertyInfo[] properties = GetProperties(obj);

                if(properties == null || properties.Length == 0)
                {
                    return null;
                }

                if((xPathElementIndex + 1) < xpath.Count)
                {
                    foreach (PropertyInfo propertyInfo in properties)
                    {
                        object value = propertyInfo.GetValue(obj, null);

                        if (value is IList)
                        {
                            IList list = (IList) value;

                            foreach (object element in list)
                            {
                                objectTrace.Add(element);
                                object result = SelectObject(objectTrace, xpath, xPathElementIndex + 1);

                                if (result != null)
                                {
                                    return result;
                                }
                                else
                                {
                                    objectTrace.RemoveAt(objectTrace.Count - 1);
                                }
                            }
                        }
                        else
                        {
                            objectTrace.Add(value);
                            object result = SelectObject(objectTrace, xpath, xPathElementIndex + 1);

                            if(result != null)
                            {
                                return result;
                            }
                            else
                            {
                                objectTrace.RemoveAt(objectTrace.Count - 1);
                            }
                        }
                    }

                    return null;
                }
                else
                {
                    object value = properties[0].GetValue(objectTrace.Last(), null);
                    return value;
                }

                #endregion Any name (*)
            }
            else if(xpath[xPathElementIndex].ElementType == PathElement.ePathElementType.AnyPlace)
            {
                #region Any Place (//)

                if (xPathElementIndex != 0)
                {
                    throw new ArgumentException("XPath root element must only be at the beginning of XPath");
                }

                if ((xPathElementIndex + 1) < xpath.Count)
                {
                    object value = TraverseAndSelectObject(objectTrace.First(), xpath, xPathElementIndex + 1);
                    return value;
                }
                else
                {
                    return null;
                }

                #endregion Any Place (//)
            }
            else if(xpath[xPathElementIndex].ElementType == PathElement.ePathElementType.Axis)
            {
                //TODO implement axis
                return null;
            }
            else if(xpath[xPathElementIndex].ElementType == PathElement.ePathElementType.Root)
            {
                #region Root (/)

                if (xPathElementIndex != 0)
                {
                    throw new ArgumentException("XPath root element must only be at the beginning of XPath");
                }

                if((xPathElementIndex + 1) < xpath.Count)
                {
                    // start at the root
                    return SelectObject(objectTrace.CloneAndOnlyKeepFirst(), xpath, xPathElementIndex + 1);
                }
                else
                {
                    return objectTrace.First();
                }

                #endregion Root (/)
            }
            else
            {
                throw new ArgumentException("Unknown PathElement type: " + xpath[xPathElementIndex].ElementType.ToString());
            }
        }

        private object TraverseAndSelectObject(object obj, List<PathElement> xpath, int xPathElementIndex)
        {
            PropertyInfo[] properties = GetProperties(obj);

            if (properties == null || properties.Length == 0)
            {
                return null;
            }

            if ((xPathElementIndex + 1) < xpath.Count)
            {
                foreach (PropertyInfo propertyInfo in properties)
                {
                    object value = propertyInfo.GetValue(obj, null);

                    if (value is IList)
                    {
                        IList list = (IList)value;
                        List<object> objectTrace = new List<object>();

                        foreach (object element in list)
                        {
                            objectTrace.Add(element);
                            object result = SelectObject(objectTrace, xpath, xPathElementIndex + 1);

                            if (result != null)
                            {
                                return result;
                            }
                            else
                            {
                                objectTrace.RemoveAt(objectTrace.Count - 1);
                            }
                        }
                    }
                }

                return null;
            }
            else
            {
                object value = properties[0].GetValue(obj, null);
                return value;
            }

        }

        static object GetPropertyValue(object obj, PathElementObject pathElement)
        {
            PropertyInfo propertyInfo = obj.GetType().GetProperty(pathElement.Name);

            if (propertyInfo == null)
            {
                return null;
            }

            if(pathElement.Index.HasValue)
            {
                object value = propertyInfo.GetValue(obj, null);

                if(value is IList)
                {
                    IList list = (IList) value;
                    value = list[pathElement.Index.Value];
                    return value;
                }
                else
                {
                    return null;
                }
            }
            else if(pathElement.Condition != null)
            {
                object value = propertyInfo.GetValue(obj, null);

                if(value is IDictionary)
                {
                    IDictionary dictionary = (IDictionary) value;
                    value = dictionary[pathElement.Condition];
                }

                return value;
            }
            else
            {
                object value = propertyInfo.GetValue(obj, null);
                return value;
            }
        }

        static PropertyInfo[] GetProperties(object obj)
        {
            PropertyInfo[] properties = obj.GetType().GetProperties();
            return properties;
        }
    }
}